/**
 * Eva programming lanbuage.
 * AST interpreter.
 */
const Environment = require("./Environment");
const Transformer = require("./Transformer");

/**
 * Default Global Environment
 */
const GlobalEnvironment = new Environment({
  null: null,
  true: true,
  false: false,
  VERSION: "0.1.0",

  "+"(op1, op2) {
    return op1 + op2;
  },

  "-"(op1, op2 = null) {
    if (op2 == null) {
      return -op1;
    }
    return op1 - op2;
  },
  "*"(op1, op2) {
    return op1 * op2;
  },
  "/"(op1, op2) {
    return op1 / op2;
  },

  ">"(op1, op2) {
    return op1 > op2;
  },
  ">="(op1, op2) {
    return op1 >= op2;
  },
  "<"(op1, op2) {
    return op1 < op2;
  },
  "<="(op1, op2) {
    return op1 <= op2;
  },
  "="(op1, op2) {
    return op1 === op2;
  },
  // navitive function
  print(...args) {
    console.log(...args);
  },
});

/**
 * Eva interpreter.
 */
class Eva {
  /**
   * Creates an Eva instance with this global environment.
   */
  constructor(global = GlobalEnvironment) {
    this.global = global;
    this._transformer = new Transformer();
  }

  /**
   * Evaluates global code wrapping into a block.
   */
  evalGlobal(expressions) {
    return this._evalBlock(["block", expressions], this.global);
  }

  /**
   * Evaluates an expression in the given environment.
   * @param {*} exp
   * @param {*} env
   * @returns
   */
  eval(exp, env = this.global) {
    // -------------------------------------------------------------
    // Self-evaluating expression:
    if (this._isNumber(exp)) {
      return exp;
    }

    if (this._isString(exp)) {
      return exp.slice(1, -1);
    }

    // -------------------------------------------------------------
    // Block: sequence of expressions
    if (exp[0] === "begin") {
      const blockEnv = new Environment({}, env);
      return this._evalBlock(exp, blockEnv);
    }

    // -------------------------------------------------------------
    // Variable declaration:
    if (exp[0] === "var") {
      const [_, name, value] = exp;
      return env.define(name, this.eval(value, env));
    }

    // -------------------------------------------------------------
    // Variable update: (set xxx = 10)
    if (exp[0] === "set") {
      const [_, ref, value] = exp;

      if (ref[0] === "prop") {
        // 类对象实例属性
        const [_tag, instance, propName] = ref;
        const instanceEnv = this.eval(instance, env);

        return instanceEnv.define(propName, this.eval(value, env));
      }

      return env.assign(ref, this.eval(value, env));
    }

    // -------------------------------------------------------------
    // Variable access:
    if (this._isVariableName(exp)) {
      return env.lookup(exp);
    }

    // -------------------------------------------------------------
    // if-expression:
    if (exp[0] === "if") {
      const [_tag, condition, consequent, alternate] = exp;
      if (this.eval(condition, env)) {
        return this.eval(consequent, env);
      }
      return this.eval(alternate, env);
    }

    // -------------------------------------------------------------
    // while-expression:
    if (exp[0] === "while") {
      const [_tag, condition, body] = exp;
      let result;
      while (this.eval(condition, env)) {
        result = this.eval(body, env);
      }
      return result;
    }

    // -------------------------------------------------------------
    // Function-declaration：(def square () (* x x))
    //
    // Syntactic sugar for: (var square (lambda (x) (* x x)))
    if (exp[0] === "def") {
      // JIT-transpile to a varibale declaration
      const varExp = this._transformer.transformDefToLambada(exp);

      return this.eval(varExp, env);
    }

    // -------------------------------------------------------------
    // Switch-expression: (switch (condition1, block1)...)
    //
    // Syntactic sugar for nested if-expressions
    if (exp[0] === "switch") {
      const ifExp = this._transformer.transformerSwitchToIf(exp);
      return this.eval(ifExp, env);
    }

    // -------------------------------------------------------------
    // For-loop: (for init condition modifier body)
    //
    // Syntactic sugar for: (begin init (while condition (begin body modifier)))
    if (exp[0] === "for") {
      const whileExp = this._transformer.transformerForToWhile(exp);
      return this.eval(whileExp, env);
    }

    // -------------------------------------------------------------
    // Increment: (++ foo)
    //
    // Syntactic sugar for: (set foo (+ foo 1))
    if (exp[0] === "++") {
      const setExp = this._transformer.transformerIncToSet(exp);
      return this.eval(setExp, env);
    }

    // -------------------------------------------------------------
    // Increment: (-- foo)
    //
    // Syntactic sugar for: (set foo (- foo 1))
    if (exp[0] === "--") {
      const setExp = this._transformer.transformerDecToSet(exp);
      return this.eval(setExp, env);
    }

    // -------------------------------------------------------------
    // Increment: (+= foo inc)
    //
    // Syntactic sugar for: (set foo (+ foo inc))
    if (exp[0] === "+=") {
      const setExp = this._transformer.transformerIncValToSet(exp);
      return this.eval(setExp, env);
    }

    // -------------------------------------------------------------
    // Increment: (-= foo dec)
    //
    // Syntactic sugar for: (set foo (- foo dec))
    if (exp[0] === "-=") {
      const setExp = this._transformer.transformerDecValToSet(exp);
      return this.eval(setExp, env);
    }

    // -------------------------------------------------------------
    // Lambda function： (lambda (x) (* x x)) 就像匿名函数
    //
    // IIFE： Immediately-invoked lambda expression: (IIFE：自执行函数)
    if (exp[0] === "lambda") {
      const [_tag, params, body] = exp;

      return {
        params,
        body,
        env, // Closure
      };
    }

    // -------------------------------------------------------------
    // Class declaration：(class <Name> <Parent> <Body>)
    if (exp[0] === "class") {
      const [_tag, name, parent, body] = exp;
      const parentEnv = this.eval(parent, env) || env;
      const classEnv = new Environment({}, parentEnv);

      this._evalBody(body, classEnv);

      return env.define(name, classEnv);
    }

    // -------------------------------------------------------------
    // Super expressions: (super <ClassName>)
    if (exp[0] === "super") {
      const [_tag, className] = exp;
      return this.eval(className, env).parent;
    }

    // -------------------------------------------------------------
    // Class instantiation: (new <Class> <Arguments>...)
    if (exp[0] === "new") {
      const classEnv = this.eval(exp[1], env);

      // An instatnce of a class is an environment
      // The `parent` component of the instance envionment
      // is set to it's class.
      const instanceEnv = new Environment({}, classEnv);

      const args = exp.slice(2).map((arg) => this.eval(arg, env));

      this._callUserDefineFunction(classEnv.lookup("constructor"), [
        instanceEnv,
        ...args,
      ]);

      return instanceEnv;
    }

    // -------------------------------------------------------------
    // Property access: (prop <instance> <name>)  类实例的属性
    if (exp[0] === "prop") {
      const [_tag, instance, name] = exp;
      const instanceEnv = this.eval(instance, env);
      return instanceEnv.lookup(name);
    }

    // -------------------------------------------------------------
    // Function-calls：
    //
    // (print "hello")
    // (+ > 5)
    // (> foo bar)
    if (Array.isArray(exp)) {
      const fn = this.eval(exp[0], env);
      const args = exp.slice(1).map((arg) => this.eval(arg, env));

      // 1. Navtive function:
      if (typeof fn === "function") {
        return fn(...args);
      }
      // 2. User-defined function:
      // (square 2)  调用
      return this._callUserDefineFunction(fn, args);
    }

    throw `Unimplemented: ${JSON.stringify(exp)}`;
  }

  _callUserDefineFunction(fn, args) {
    const activationRecord = {};
    fn.params.forEach((param, index) => {
      activationRecord[param] = args[index];
    });

    const activationEnv = new Environment(activationRecord, fn.env);
    return this._evalBody(fn.body, activationEnv);
  }

  _evalBody(body, env) {
    if (body[0] === "begin") {
      return this._evalBlock(body, env);
    }
    return this.eval(body, env);
  }

  _evalBlock(block, env) {
    let result;
    const [_tag, ...expressions] = block;

    expressions.forEach((exp) => {
      result = this.eval(exp, env);
    });
    return result;
  }

  _isNumber(exp) {
    return typeof exp === "number";
  }

  _isString(exp) {
    return typeof exp === "string" && exp[0] === '"' && exp.slice(-1) === '"';
  }

  _isVariableName(exp) {
    return typeof exp === "string" && /^[+\-*/<>=a-zA-Z0-9_]*$/.test(exp);
  }
}

module.exports = Eva;
